/**
 * Copyright 2013 Douglas Lacher
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * @author Douglas Lacher <doug.lacher@gmail.com>
 */

package com.devioustoast.andmoco;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import android.annotation.TargetApi;
import android.app.ActionBar;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.os.Bundle;
import android.os.Environment;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.os.Build;
import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentTransaction;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ArrayAdapter;

public class MainActivity extends FragmentActivity implements
    ActionBar.OnNavigationListener,
    BluetoothConnector.ConnectionStateChangeListener {

  public static final String LOG_TAG = "AndMoCo";

  /**
   * The serialization (saved instance state) Bundle key representing the
   * current dropdown position.
   */
  private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item";
  private static final String PREFERENCE_BLUETOOTH_ADDRESS = "bluetooth_device_address";
  private static final String IS_EXTERNAL_STORAGE_SETUP = "is_external_storage_setup";
  private static final String ANDMOCO_EXTERNAL_PATH = String.format(
      "%s/%s", Environment.DIRECTORY_DOWNLOADS, "andmoco");
  private static final String MOVES_PATH = "moves";

  // These Strings would be constants if not for their references being null until onCreate().
  private String[] mMenuTitles;
  private String mTitleMoves;
  private String mTitleControlPad;
  private String mTitleCalibration;

  private SharedPreferences mPreferences;
  private BluetoothConnector mBluetoothConnector;
  private Robot mHexapod;
  private MenuItem mMenuConnectionStatus;
  private MenuItem mMenuConnectOrDisconnect;
  private File mRobotMovesDirectory;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    mPreferences = getPreferences(Context.MODE_PRIVATE);
    try {
      mRobotMovesDirectory = setupExternalStorageDirectory(
          ANDMOCO_EXTERNAL_PATH,
          MOVES_PATH,
          mPreferences.getBoolean(IS_EXTERNAL_STORAGE_SETUP, false));
    } catch (IOException e) {
      Log.e(LOG_TAG, "IO Error during initial setup of the external storage directory.", e);
    }

    mMenuTitles = getResources().getStringArray(R.array.menu_titles);
    mTitleMoves = getResources().getString(R.string.moves);
    mTitleControlPad = getResources().getString(R.string.control_pad);
    mTitleCalibration = getResources().getString(R.string.calibration);
    setContentView(R.layout.activity_main);

    ActionBar actionBar = getActionBar();
    actionBar.setDisplayShowTitleEnabled(false);
    actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
    // TODO(dlacher): implement actionBar.setLogo() and/or actionBar.setIcon().
    actionBar.setListNavigationCallbacks(
        ArrayAdapter.createFromResource(
            getActionBarThemedContextCompat(),
            R.array.menu_titles,
            android.R.layout.simple_list_item_1),
        this);

    BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
    if (bluetoothAdapter == null) {
      warnUserBluetoothRequiredAndExit();
    }
    if (!bluetoothAdapter.isEnabled()) {
      enableBluetooth(bluetoothAdapter);
    }
    mBluetoothConnector = new BluetoothConnector(bluetoothAdapter);
    mBluetoothConnector.registerListener(this);
    mHexapod = new Robot(mBluetoothConnector, mPreferences);
    mBluetoothConnector.start();
  }

  @Override
  protected void onStart() {
    super.onStart();
    try {
      mRobotMovesDirectory = setupExternalStorageDirectory(
          ANDMOCO_EXTERNAL_PATH,
          MOVES_PATH,
          mPreferences.getBoolean(IS_EXTERNAL_STORAGE_SETUP, false));
      mHexapod.scanMovesSourcesAndBuildMoves(mRobotMovesDirectory, getAssets(), MOVES_PATH);
    } catch (IOException e) {
      Log.e(LOG_TAG, "IO Error during initial setup of the external storage directory.", e);
    }
    registerReceiver(mBluetoothConnector.receiver, new IntentFilter(BluetoothDevice.ACTION_FOUND));
    mBluetoothConnector.connect();
  }

  @Override
  protected void onResume() {
    super.onResume();
    mBluetoothConnector.setRun(true);
  }

  @Override
  protected void onPause() {
    super.onPause();
    mBluetoothConnector.setRun(false);
  }

  @Override
  protected void onStop() {
    unregisterReceiver(mBluetoothConnector.receiver);

    mBluetoothConnector.disconnect();
    mBluetoothConnector.getBluetoothAdapter().cancelDiscovery();

    BluetoothDevice bluetoothDevice = mBluetoothConnector.getBluetoothDevice();
    SharedPreferences.Editor preferencesEditor = mPreferences.edit();
    preferencesEditor.putString(PREFERENCE_BLUETOOTH_ADDRESS, bluetoothDevice.getAddress());
    preferencesEditor.commit();

    super.onStop();
  }

  /**
   * Backward-compatible version of {@link ActionBar#getThemedContext()} that
   * simply returns the {@link android.app.Activity} if
   * <code>getThemedContext</code> is unavailable.
   */
  @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
  private Context getActionBarThemedContextCompat() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
      return getActionBar().getThemedContext();
    } else {
      return this;
    }
  }

  /**
   * Handles restoring instance state between runs.
   */
  @Override
  public void onRestoreInstanceState(Bundle savedInstanceState) {
    if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM)) {
      getActionBar().setSelectedNavigationItem(
          savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM));
    }
  }

  /**
   * Handles saving instance state between runs.
   */
  @Override
  public void onSaveInstanceState(Bundle outState) {
    outState.putInt(
        STATE_SELECTED_NAVIGATION_ITEM,
        getActionBar().getSelectedNavigationIndex());
  }

  /**
   * Creates the options menu (right side of UI).
   */
  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.activity_main, menu);
    mMenuConnectionStatus = menu.findItem(R.id.menu_connection_status);
    mMenuConnectOrDisconnect = menu.findItem(R.id.menu_connect_or_disconnect);
    return true;
  }

  /**
   * Handles click events for the options menu (top-right of UI).
   */
  @Override
  public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
      case R.id.menu_connection_status:
        if (!mBluetoothConnector.isConnected()) {
          mBluetoothConnector.connect();
        }
        break;
      case R.id.menu_connect_or_disconnect:
        if (mBluetoothConnector.isConnected()) {
          mBluetoothConnector.disconnect();
        } else {
          mBluetoothConnector.connect();
        }
        break;
      case R.id.menu_quit:
        finish();
        break;
      default:
        return super.onOptionsItemSelected(item);
    }
    return true;
  }

  /**
   * Handles UI navigation events from the action bar (top-left of UI).
   */
  @Override
  public boolean onNavigationItemSelected(int position, long id) {
    FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
    String title = mMenuTitles[position];
    if (mTitleMoves.equals(title)) {
      MovesFragment fragment = new MovesFragment();
      fragment.setRobot(mHexapod);
      transaction = transaction.replace(R.id.container, fragment);
    } else if (mTitleControlPad.equals(title)) {
      ControllerFragment fragment = new ControllerFragment();
      fragment.setRobot(mHexapod);
      transaction = transaction.replace(R.id.container, fragment);
    } else if (mTitleCalibration.equals(title)){
      CalibrationFragment fragment = new CalibrationFragment();
      fragment.setRobot(mHexapod);
      transaction = transaction.replace(R.id.container, fragment);
    } else {
      Log.e(LOG_TAG, String.format("Unknown menu title, \"%s\".", title));
      return false;
    }
    transaction.commit();
    return true;
  }

  /**
   * Handles activity callbacks.
   *
   * Current callbacks implemented:
   *   REQUEST_ENABLE_BT - Request to enable bluetooth.
   */
  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (BluetoothConnector.REQUEST_ENABLE_BT == requestCode) {
      if (RESULT_CANCELED == resultCode) {
        warnUserBluetoothRequiredAndExit();
      }
    }
  }

  /**
   * Handles screen rotation events.
   */
  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    setContentView(R.layout.activity_main);
    onNavigationItemSelected(getActionBar().getSelectedNavigationIndex(), 0);
  }

  /**
   * Handles Bluetooth connection state change events.
   *
   * @param newState the new 
   */
  @Override
  public void onConnectionStateChange(final BluetoothConnector.ConnectionState newState) {
    if (mMenuConnectionStatus == null) {
      return;
    }
    runOnUiThread(new Runnable() {
      @Override
      public void run() {
        switch (newState) {
        case CONNECTED:
          Log.d(LOG_TAG, "Connection state has changed to connected.");
          mMenuConnectionStatus.setIcon(R.drawable.green_dot);
          mMenuConnectionStatus.setTitle(R.string.menu_connection_connected);
          mMenuConnectOrDisconnect.setTitle(R.string.menu_disconnect);
          break;
        case CONNECTING:
          Log.d(LOG_TAG, "Connection state has changed to connecting.");
          mMenuConnectionStatus.setIcon(R.drawable.yellow_dot);
          mMenuConnectionStatus.setTitle(R.string.menu_connection_connecting);
          mMenuConnectOrDisconnect.setTitle(R.string.menu_disconnect);
          break;
        case DISCONNECTED:
          Log.d(LOG_TAG, "Connection state has changed to disconnected.");
          mMenuConnectionStatus.setIcon(R.drawable.red_dot);
          mMenuConnectionStatus.setTitle(R.string.menu_connection_disconnected);
          mMenuConnectOrDisconnect.setTitle(R.string.menu_connect);
          break;
        default:
          Log.wtf(
              LOG_TAG,
              String.format(
                  "Received unknown ConnectionState, \"%s\" for BluetoothConnector.", newState));
        }
      }});
  }

  /**
   * Returns true if external storage is writable.
   */
  private boolean isExternalStorageWritable() {
    return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
  }

  /**
   * Returns true if external storage is mounted read-only.
   */
  @SuppressWarnings("unused")
  private boolean isExternalStorageReadOnly() {
    return Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment.getExternalStorageState());
  }

  /**
   * Returns a handle to the directory name on the external storage device.
   *
   * The directory name will be created at the root of the external storage device if it doesn't
   * yet exist.
   *
   * @param directoryName the name of the directory to return.
   * @return a File handle for the directory.
   */
  private File getExternalStorageDirectory(String directoryName) {
    File file = Environment.getExternalStoragePublicDirectory(directoryName);
    if (!file.exists() && !file.mkdirs()) {
        Log.e(LOG_TAG, String.format("Directory \"%s\" not created"));
    }
    return file;
  }

  /**
   * Sets up the external storage (i.e. SD card) directory, if external storage is available.
   *
   * This method will attempt to create the external directory and, if noCopyFalse is false, copy
   * the files from the assets directory to the external directory.
   *
   * @param externalDirectoryName the name of the directory to create on the external storage device.
   * @param assetsDirectoryName name of the assets directory whose contents will be copied to the
   *        external storage device directory.
   * @param noCopyFiles set false to copy files to the external storage directory.
   * @return a File object representing the external storage directory or null if external storage 
   * @throws IOException if there was a problem copying files.
   */
  private File setupExternalStorageDirectory(
      String externalDirectoryName, String assetsDirectoryName, boolean noCopyFiles)
          throws IOException {
    File directory = getExternalStorageDirectory(externalDirectoryName);
    // The order of this condition is significant.
    if (!noCopyFiles && isExternalStorageWritable() && directory.exists() && directory.canWrite()) {
      copyAssetsToDirectory(assetsDirectoryName, directory);
      SharedPreferences.Editor editor = mPreferences.edit();
      editor.putBoolean(IS_EXTERNAL_STORAGE_SETUP, true);
      editor.commit();
    }
    return directory;
  }

  /**
   * Copies the contents of the named assets directory into the provided directory file handle.
   *
   * It is the contents of the directory that will be copied, not the directory itself.
   *
   * @param assetsSourceDirectory the name of the source assets directory.
   * @param directory a handle to the destination directory.
   * @return true if all files were copied or false if there was an error.
   * @throws IOException if there was a problem copying files.
   */
  private void copyAssetsToDirectory(String assetsSourceDirectory, File directory)
      throws IOException {
    String[] assetList = getAssets().list(assetsSourceDirectory);
    String fileName = null;
    for (String asset : assetList) {
      String assetPath = String.format("%s/%s", assetsSourceDirectory, asset);
      Log.d(LOG_TAG, String.format(
          "Copying asset %s to %s.", assetPath, directory.getAbsolutePath()));
      InputStream inputStream = null;
      OutputStream outputStream = null;
      fileName = new File(directory.getAbsolutePath(), asset).getAbsolutePath();
      inputStream = getAssets().open(assetPath);
      outputStream = new FileOutputStream(fileName);
      copyFile(inputStream, outputStream);
      inputStream.close();
      outputStream.flush();
      outputStream.close();
      inputStream = null;
      outputStream = null;
    }
  }

  /**
   * Copies a file from an input stream to an output stream.
   *
   * @param inputStream an open input stream from which to copy.
   * @param outputStream an open output stream to write.
   * @throws IOException if there was an error reading or writing the files.
   */
  private void copyFile(InputStream inputStream, OutputStream outputStream) throws IOException {
    byte[] buffer = new byte[1024];
    while (inputStream.read(buffer) > -1) {
      outputStream.write(buffer);
    }
  }

  /**
   * Enables the Bluetooth service.  Blocks while waiting.
   */
  private void enableBluetooth(BluetoothAdapter adapter) {
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, BluetoothConnector.REQUEST_ENABLE_BT);
  }

  /**
   * Displays a pop-up error in the UI informing the user that Bluetooth is required and
   * exits the app after the user has acknowledged the error.
   */
  private void warnUserBluetoothRequiredAndExit() {
    new AlertDialog.Builder(this)
        .setCancelable(false)
        .setTitle(R.string.bluetooth_required_title)
        .setMessage(R.string.bluetooth_required_message)
        .show();
    finish();
  }
}
